S08-06 Node-Koa2
[TOC]
概述
定位: node.js 的下一代 web 框架
前面我们已经学习了 express,另外一个非常流行的 Node Web 服务器框架就是 Koa。
事实上,koa 是 express 同一个团队开发的一个新的 Web 框架:
- 目前团队的核心开发者 TJ 的主要精力也在维护 Koa,express 已经交给团队维护了;
- Koa 旨在为 Web 应用程序和 API 提供更小、更丰富和更强大的能力;
- 相对于 express 具有更强的异步处理能力(后续我们再对比);
- Koa 的核心代码只有 1600+行,是一个更加轻量级的框架,我们可以根据需要安装和使用中间件;
koa
依赖包: koa
安装: npm i koa
基本使用
const Koa = require('koa')
// 1、创建app对象
const app = new Koa()
// 4、注册中间件
app.use((ctx, next) => {
console.log('匹配到中间件1')
// 5、返回数据
ctx.body = '中间件1'
})
// 2、启动服务器
app.listen(8000, () => {
console.log('koa is running~')
})
// 3、运行服务器
中间件
说明: 和 express 中间件具有相同的功能
语法:
app.use((ctx, next) => {
console.log('middleware 01')
next()
})
app.use((ctx, next) => {
console.log('middleware 02')
ctx.response.body = 'Hello World'
})
参数:
- ctx:
,上下文(Context)对象,包括请求对象
req
和响应对象res
,见ctx - next:``,本质上是一个 dispatch,类似于之前的 next
返回值: undefined
注意: koa 原生注册中间件只能通过 use() 方法,没有提供 methods 的方式,也没有提供 path 中间件来匹配路径
app.use((ctx, next) => {
console.log('middleware 02')
ctx.response.body = 'Hello World'
})
路由
说明:koa 官方并没有给我们提供路由的库,我们可以选择第三方库:@koa/router
分离 path、method
真实开发中我们如何将路径和 method 分离呢?
方式一:根据 request 自己判断
app.use((ctx, next) => {
if (ctx.request.path === '/users') {
if (ctx.request.method === 'POST') {
ctx.response.body = 'Create User Success~'
} else {
ctx.response.body = 'Users List~'
}
} else {
ctx.response.body = 'Other Request Response'
}
})
整个代码的逻辑是非常复杂和混乱的,真实开发中我们会使用路由。
方式二: 使用第三方路由中间件
@koa/router
安装
依赖包: @koa/router
安装: pnpm add @koa/router
注意: 过气的依赖包:koa-router
基本使用
const Koa = require('koa')
const KoaRouter = require('@koa/router')
const app = new Koa()
// 1. 创建路由对象
const userRouter = new KoaRouter({ prefix: '/users' })
// 2. 注册路由中间件
userRouter.get('/', (ctx, next) => {
ctx.body = 'user列表'
})
userRouter.get('/:id', (ctx, next) => {
const id = ctx.params.id
ctx.body = '获取某个用户的信息:' + id
})
userRouter.post('/', (ctx, next) => {
ctx.body = '创建用户成功~'
})
userRouter.delete('/:id', (ctx, next) => {
const id = ctx.params.id
ctx.body = '删除用户成功~' + id
})
userRouter.patch('/:id', (ctx, next) => {
const id = ctx.params.id
ctx.body = '修改用户成功~' + id
})
// 3. 挂载路由
app.use(userRouter.routes())
app.listen(8000, () => {
console.log('koa is running~')
})
封装路由
1、创建userRouter.js
文件
const KoaRouter = require('@koa/router')
// 1. 创建路由对象
const userRouter = new KoaRouter({ prefix: '/users' })
// 2. 注册路由中间件
userRouter.get('/', (ctx, next) => {
ctx.body = 'user列表'
})
userRouter.get('/:id', (ctx, next) => {
const id = ctx.params.id
ctx.body = '获取某个用户的信息:' + id
})
userRouter.post('/', (ctx, next) => {
ctx.body = '创建用户成功~'
})
userRouter.delete('/:id', (ctx, next) => {
const id = ctx.params.id
ctx.body = '删除用户成功~' + id
})
userRouter.patch('/:id', (ctx, next) => {
const id = ctx.params.id
ctx.body = '修改用户成功~' + id
})
// 3. 导出userRouter
module.exports = userRouter
2、导入路由模块并挂载
const Koa = require('koa')
// 4. 导入userRouter
const userRouter = require('./d03-userRouter')
const app = new Koa()
// 5. 挂载路由
app.use(userRouter.routes())
app.listen(8000, () => {
console.log('koa is running~')
})
allowedMethods
allowedMethods
用于判断某一个method
是否支持:
- 如果我们请求 get,那么是正常的请求,因为我们有实现 get;
- 如果我们请求 put、delete、patch,那么就自动报错:
Method Not Allowed
,状态码:405; - 如果我们请求 link、copy、lock,那么就自动报错:
Not Implemented
,状态码:501;
router 前缀
通常一个路由对象是对一组相似路径的封装,那么路径的前缀都是一直的,所以我们可以直接在创建 Router 时,添加前缀
+ const userRouter = new Router({ prefix: '/users' })
userRouter.get('/', (ctx, next) => { // 匹配/users/
ctx.response.body = 'user list~'
})
userRouter.get('/:id', (ctx, next) => { // 匹配/users/:id
const id = ctx.params.id
ctx.body = '获取某个用户的信息:' + id
})
module.exports = userRouter
API
const router = new Router({ prefix: '/xxx' })
new Router()
说明: 创建一个路由器(Router)实例
语法:
const router = new Router(options?)
参数:
- options?:``,
- prefix?:``,为路由器中所有路由定义的 URL 前缀。默认为 ""
返回值:
- router:``,路由实例
router.routes()
说明: 将路由器中定义的路由添加到 Koa 应用程序中
语法:
router.routes()
参数: void
返回值: 返回一个路由中间件
示例:
// 将路由器中定义的路由添加到Koa应用程序中
app.use(router.routes())
router.METHODS()
说明: 包括一系列的请求方法:
- router.get():``
- router.post():``
- router.delete():``
- router.patch():``
- router.put():``
- router.head():``
- router.options():``
语法:
router.get(path, middleware)
参数:
- path:
string | reg
,要匹配的 URL 路径模式 - middleware:``,中间件函数,可以添加多个中间件
返回值: undefined
示例:
// 定义一个简单的 GET 请求路由
router.get('/hello', (ctx, next) => {
ctx.body = 'Hello, Koa!'
})
router.allowedMethods()
说明: 一个中间件函数,用于处理在路由处理之后、但未发送响应之前的阶段。它的作用是根据请求的方法(GET、POST、PUT 等)来检查路由是否允许该方法,并进行适当的处理。
未在后端封装的方法,前端请求时会返回Method Not Allowed
,而不是Not Found
语法:
router.allowedMethods(options?)
参数:
- options?:``,
- throw:``,当请求方法不匹配时,是否抛出错误。默认为 true
- notImplemented:``,未实现的 HTTP 方法的响应状态码。默认为 501
- methodNotAllowed:``,不允许的 HTTP 方法的响应状态码。默认为 405
返回值: 返回一个路由中间件
示例:
// 定义路由
router.get('/users', (ctx, next) => {
// 处理 GET /users 请求
})
router.post('/users', (ctx, next) => {
// 处理 POST /users 请求
})
// 加载路由中间件
app.use(router.routes())
;+app.use(router.allowedMethods())
请求
ctx
说明: ctx 是指当前请求的上下文对象,它是一个可读写的 JavaScript 对象。ctx 包含了当前请求的所有信息,如请求头、请求体、URL 等等。在中间件函数中,我们可以通过 ctx 对象来访问这些信息。
属性:
- Node 原生对象
- ctx.req:原生 Node.js 的 request 对象。
- ctx.res:原生 Node.js 的 response 对象。
- Koa 封装对象
- ctx.request:Koa 封装后的请求对象,包含了请求头、请求体等信息。
- ctx.response:Koa 封装后的响应对象,包含了响应头、响应体等信息。
- 请求
- ctx.method:HTTP 请求的方法,如 GET、POST 等。
- ctx.url:请求的 URL 地址,不包含域名部分。
- ctx.path:请求的路径,不包含查询参数部分。
- 获取请求参数
- ctx.query:请求的 query 参数,以对象形式返回。
- ctx.params:请求的 params 参数
- ctx.request.body:请求的 post 参数,包括 json, x-www-form-urlencoded, form-data
- 响应
- ctx.body:响应的主体内容。
- ctx.status:响应的状态码,默认为 404。
- ctx.type:设置响应的
Content-Type
头部字段 - ctx.app:获取当前应用程序的实例
- ctx.cookies:Cookies 对象,用于读取和设置 Cookies。
请求参数解析
客户端传递到服务器参数的方法常见的是 5 种:
- 方式一:通过 get 请求中的 URL 的 params;
- 方式二:通过 get 请求中的 URL 的 query;
- 方式三:通过 post 请求中的 body 的 json 格式;
- 方式四:通过 post 请求中的 body 的 x-www-form-urlencoded 格式;
- 方式五:通过 post 请求中的 form-data 格式;
GET:query string
请求地址:/login?username=why&password=123
获取参数:通过 ctx.query 或 ctx.request.query 获取参数
注意: 通过ctx.query
获取的参数是对象格式
// 1. query string
userRouter.get('/', (ctx, next) => {
console.log(ctx.query) // { name: 'tom', age: '18' }
console.log(ctx.request.query) // { name: 'tom', age: '18' }
console.log(ctx.query === ctx.request.query) // true
ctx.body = JSON.stringify(ctx.query)
})
GET:params
请求地址:/users/123
获取参数:通过 ctx.params 配合动态路由/:id
获取参数 123
// 2. params
const userRouter = new Router({ prefix: '/users' })
userRouter.get('/:id', (ctx, next) => {
console.log(ctx.params.id) // 123
ctx.body = 'Hello World'
})
POST:json
请求地址:/login
请求参数: body 是 json 格式:
{
"username": "coderwhy",
"password": "123"
}
获取参数:
依赖包:koa-bodyparser
安装:pnpm add koa-bodyparser
1、挂载中间件
koa-bodyparser
2、通过 ctx.rquest.body 获取 json 参数
注意: 不能从ctx.body
或ctx.req.body
中解析数据
// 3. json
app.use(bodyParser())
app.use((ctx, next) => {
console.log(ctx.request.body)
ctx.body = 'Hello World'
})
POST:x-www-form-urlencoded
请求地址: /login
请求参数: body 是 x-www-form-urlencoded 格式
获取参数: 和 json 是一致的
依赖包:koa-bodyparser
安装:pnpm add koa-bodyparser
1、挂载中间件
koa-bodyparser
2、通过 ctx.rquest.body 获取 json 参数
注意: 不能从ctx.body
或ctx.req.body
中解析数据
app.use(bodyParser())
app.use((ctx, next) => {
console.log(ctx.request.body)
ctx.body = 'Hello World'
})
POST:form-data
请求地址:/login
请求参数: body 是 form-data 格式
获取参数:
依赖包:@koa/multer
multer
安装:pnpm add @koa/multer multer
- 1、挂载中间件
@koa/multer
- 2、通过 ctx.request.body 获取 form-data 参数
// 1、挂载中间件`@koa/multer`
const multer = require('@koa/multer')
const upload = multer({})
app.use(upload.any())
// 2、通过 ctx.request.body 获取form-data参数
app.use((ctx, next) => {
console.log(ctx.request.body)
ctx.body = ctx.request.body
})
form-data 实现文件上传
我们知道 multer 还可以实现文件的上传:见文件上传
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, './uploads/')
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname))
}
})
const upload = multer({
storage
})
const fileRouter = new Router()
fileRouter.post('/upload', upload.single('avatar'), (ctx, next) => {
console.log(ctx.req.file)
})
app.use(fileRouter.routes())
响应
响应方式:
- ctx.body :Koa 中通过
ctx.body
响应数据
响应数据类型
string
:字符串数据Buffer
:Buffer 数据Stream
:流数据Object
||Array
:对象或者数组null
:不输出任何内容
示例:
// 1. string
ctx.response.body = 'Hello World'
// 2. object
ctx.body = {
name: 'why',
age: 18,
height: 1.88
}
// 3. array
ctx.body = ['tom', 'jack', 'mark']
// 4. stream
const readableStreamTxt = fs.createReadStream('./data/stream.txt')
const readableStreamImg = fs.createReadStream('./data/plane.webp')
console.log(readableStreamImg)
ctx.type = 'image/webp'
ctx.body = readableStreamImg
// 5. buffer
const buf = Buffer.from('你好,Koa')
ctx.body = buf
// 6. null
ctx.body = null
对比 ctx.response.body、ctx.body
ctx.body === ctx.response.body
ctx.body !== ctx.request.body
- 事实上,我们访问 ctx.body 时,本质上是访问 ctx.response.body
- 我们可以看到源码中,我们访问 proto(这里就是 ctx),其实是访问 proto 中的 response 的属性
响应状态设置
设置方式:
即可以通过 ctx.status
设置,也可以通过ctx.response.status
设置
- ctx.status
- ctx.response.status
注意: 如果ctx.status
尚未设置,Koa 会自动将状态设置为200
或204
。
文件上传
单文件上传
@koa/multer
依赖包: @koa/multer
安装: pnpm add @koa/multer
基本使用
思路:
- 1、通过
multer({dest})
的dest
选项设置上传文件的目标目录 - 2、通过在
/single
路由中调用upload.single('plane')
中间件,实现单文件上传 - 3、单文件的信息可以通过
ctx.file
数组查看
注意: 通过dest
的方法无法自定义文件名,并且上传的文件没有后缀名
const Koa = require('koa')
const KoaRouter = require('@koa/router')
const multer = require('@koa/multer')
const app = new Koa()
const uploadRouter = new KoaRouter({ prefix: '/upload' })
// 文件上传-单文件
+ const upload = multer({ dest: './upload' })
uploadRouter.post('/single', upload.single('plane'), (ctx, next) => {
console.log(ctx.file)
if (ctx.file.filename) {
ctx.body = '文件上传成功~'
}
})
app.use(uploadRouter.routes())
app.listen(8000, () => {
console.log('koa is running~')
})
优化:自定义文件名
思路: 通过multer({storage: multer.diskStorage(destination, filename)})
的方式设置上传文件的目标目录,可以自定义文件名,并且上传的文件也有后缀名
// 文件上传-单文件
const upload = multer({
storage: multer.diskStorage({
destination(req, file, callback) {
callback(null, './upload')
},
filename(req, file, callback) {
const newName =
file.originalname.replace(path.extname(file.originalname), '') +
'_' +
Date.now() +
path.extname(file.originalname)
console.log(newName)
callback(null, newName)
}
})
})
uploadRouter.post('/single', upload.single('plane'), (ctx, next) => {
console.log(ctx.file)
if (ctx.file.filename) {
ctx.body = '文件上传成功~'
}
})
多文件上传
思路:
- 1、通过
multer({storage: multer.diskStorage(destination, filename)})
的方式设置上传文件的目标目录(和单文件一样) - 2、通过在
/multi
路由中调用upload.array('vision')
中间件,实现多文件上传 - 3、多文件的信息可以通过
ctx.files
数组查看
const path = require('path')
const Koa = require('koa')
const KoaRouter = require('@koa/router')
const multer = require('@koa/multer')
const app = new Koa()
const uploadRouter = new KoaRouter({ prefix: '/upload' })
// 文件上传-多文件
const upload = multer({
storage: multer.diskStorage({
destination(req, file, callback) {
callback(null, './upload')
},
filename(req, file, callback) {
const newName =
file.originalname.replace(path.extname(file.originalname), '') +
'_' +
Date.now() +
path.extname(file.originalname)
console.log(newName)
callback(null, newName)
}
})
})
uploadRouter.post('/multi', upload.array('vision'), (ctx, next) => {
console.log(ctx.files)
ctx.body = '文件上传成功~'
})
app.use(uploadRouter.routes())
app.listen(8000, () => {
console.log('koa is running~')
})
静态服务器
koa-static
依赖包: koa-static
安装: pnpm add koa-static
基本使用
部署的过程类似于 express
const Koa = require('koa')
const static = require('koa-static')
const app = new Koa() + app.use(static('./build'))
app.listen(8000, () => {
console.log('静态服务器启动成功~')
})
错误处理
1、在需要抛出错误的位置通过 ctx.app.emit() 发射 error 事件
- ctx.app :可以获取当前应用程序的实例
- app.emit(eventName, ...args):触发事件并传递参数给监听器回调函数。
userRouter.get('/', (ctx, next) => {
const isError = true
if (isError) {
// 1. 发射错误事件
ctx.app.emit('error', -1004, ctx)
} else {
ctx.body = '用户列表~'
}
})
2、在统一的位置处理抛出的错误
- app.on(eventName, listener):注册一个事件监听器,当事件被触发时执行回调函数。
// 2. 监听事件,集中处理错误信息
app.on('error', (code, ctx) => {
let message = ''
switch (code) {
case -1001:
message = '请求地址错误'
break
case -1002:
message = '资源错误'
break
default:
message = '其他错误'
}
ctx.body = {
code,
message
}
})
示例:
const Koa = require('koa')
const KoaRouter = require('@koa/router')
const app = new Koa()
const userRouter = new KoaRouter({ prefix: '/users' })
/* 错误处理 */
userRouter.get('/', (ctx, next) => {
const isError = true
if (isError) {
// 1. 发射错误事件
ctx.app.emit('error', -1004, ctx)
} else {
ctx.body = '用户列表~'
}
})
// 2. 监听事件,集中处理错误信息
app.on('error', (code, ctx) => {
let message = ''
switch (code) {
case -1001:
message = '请求地址错误'
break
case -1002:
message = '资源错误'
break
case -1003:
message = '权限错误'
break
default:
message = '其他错误'
}
ctx.body = {
code,
message
}
})
app.use(userRouter.routes())
app.listen(8000, () => {
console.log('koa is running~')
})
对比 express
架构设计
- express:完整和强大的,内置了很多好用的功能
- koa:简洁和自由的,只包含最核心功能,不限制使用其他中间件
执行同步
express 和 koa 框架的核心都是中间件,但是他们的执行机制不同,特别是中间件中包含异步操作时
**需求:**假如有三个中间件会在一次请求中匹配到,并且按照顺序执行;
- 在 middleware1 中,在 req.message 中添加一个字符串
aaa
- 在 middleware2 中,在 req.message 中添加一个 字符串
bbb
- 在 middleware3 中,在 req.message 中添加一个 字符串
ccc
- 当所有内容添加结束后,在 middleware1 中,通过 res 返回最终的结果
- 注意: 是在 middleware1 中返回 res
1、通过 express 同步实现
const express = require('express')
const app = express()
const middleware1 = (req, res, next) => {
req.message = 'aaa'
next()
+ res.end(req.message) // aaabbbccc
}
const middleware2 = (req, res, next) => {
+ req.message = req.message + 'bbb'
next()
}
const middleware3 = (req, res, next) => {
+ req.message = req.message + 'ccc'
}
app.use(middleware1, middleware2, middleware3)
app.listen(8000, () => {
console.log('启动成功~')
})
最终的结果是:aaabbbccc
,没问题
2、通过 koa 同步实现
const Koa = require('koa')
const app = new Koa()
const middleware1 = (ctx, next) => {
+ ctx.message = 'aaa'
next()
console.log('aaaa')
+ ctx.body = ctx.message // aaabbbccc
}
const middleware2 = (ctx, next) => {
+ ctx.message = ctx.message + 'bbb'
console.log('bbbb')
next()
}
const middleware3 = (ctx, next) => {
+ ctx.message = ctx.message + 'ccc'
}
app.use(middleware1)
app.use(middleware2)
app.use(middleware3)
app.listen(8000, () => {
console.log('启动成功~')
})
最终的结果也是:aaabbbccc
,也没问题
执行异步
如果我们最后的 ccc 中的结果,是需要异步操作才能获取到的,是否会产生问题呢?
思路:
- koa 中如果希望等待下一个异步函数的执行结果,需要在 next 函数前加上 await
- express 中添加 await 的方法无效
原理:
- koa 中的 next()函数返回的是 Promise,因此可以使用 await
- express 中的 next()返回的是 void,因此 await 无效
1、express 中遇到异步操作
1)、没有在 next 前,加 async、await
const middleware1 = (req, res, next) => {
req.message = 'aaa'
next()
res.end(req.message)
}
const middleware2 = (req, res, next) => {
req.message = req.message + 'bbb'
next()
}
const middleware3 = async (req, res, next) => {
// 异步操作
+ const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876')
req.message = req.message + result.data.lrc.lyric
console.log(req.message)
}
最终的结果aaabbb
,是不正确。
2)、有在 next 前,加 async、await
const middleware1 = async (req, res, next) => {
req.message = 'aaa'
await next()
res.end(req.message)
}
const middleware2 = async (req, res, next) => {
req.message = req.message + 'bbb'
await next()
}
const middleware3 = async (req, res, next) => {
// 异步操作
+ const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876')
req.message = req.message + result.data.lrc.lyric
console.log(req.message)
}
最终的结果也是aaabbb
,也是不正确。
为什么呢?
- 原因是本质上的 next()和异步没有任何关系;
- 它本身就是一个同步函数的调用,所以它不会等到你异步有结果之后,再继续执行后续的操作;
2、koa 中遇到异步操作
1)没有在 next 前,加 async、await
const middleware1 = async (ctx, next) => {
ctx.message = 'aaa'
next()
ctx.body = ctx.message
}
const middleware2 = async (ctx, next) => {
ctx.message = ctx.message + 'bbb'
next()
}
const middleware3 = async (ctx, next) => {
// 异步操作
+ const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876')
ctx.message = ctx.message + result.data.lrc.lyric
}
最终的结果也是aaabbb
,也是不正确。
- 这是因为虽然 next 函数是一个返回 promise 的异步操作,但是在前面不加 await 的情况,是不同等待结果的返回,就会继续向后执行了;
2)有在 next 前,加 async、await
const middleware1 = async (ctx, next) => {
ctx.message = 'aaa'
await next()
ctx.body = ctx.message
}
const middleware2 = async (ctx, next) => {
ctx.message = ctx.message + 'bbb'
await next()
}
const middleware3 = async (ctx, next) => {
// 异步操作
+ const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876')
ctx.message = ctx.message + result.data.lrc.lyric
}
最终的结果是aaabbb+歌词信息
,是正确。
- 这是因为,当我们在 koa 中的 next 前面加 await 时,它会等到后续有一个确定结果时,在执行后续的代码;
洋葱模型
洋葱模型(Onion Model)是 Koa 框架中用来描述中间件执行流程的一种模型。这个模型得名于中间件的执行方式,就像剥洋葱一样,请求和响应穿过一系列中间件,每个中间件都有机会在请求到达和离开时执行特定的逻辑。
洋葱模型的执行顺序可以简单描述为:请求从外层中间件开始处理,然后依次向内层传递;在内层中间件处理完毕后又依次向外层传递响应。这种模型可以让开发者清晰地了解中间件的执行顺序,并便于对请求和响应进行各种处理。
注意:
Koa 中无论同步、异步都符合洋葱模型
Express 中同步符合洋葱模型,异步不符合
koa 中异步请求的执行顺序
源码
new Koa()
1、
+++ const app = new Koa()
2、new Koa()
本质上是new Application()
类
module.exports = class Application extends Emitter {
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
// 用于保存中间件fns
++ this.middleware = [];
+ this.context = Object.create(context);
+ this.request = Object.create(request);
+ this.response = Object.create(response);
// util.inspect.custom support for node 6+
/* istanbul ignore else */
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
if (options.asyncLocalStorage) {
const { AsyncLocalStorage } = require('async_hooks');
assert(AsyncLocalStorage, 'Requires node 12.17.0 or higher to enable asyncLocalStorage');
this.ctxStorage = new AsyncLocalStorage();
}
}
};
app.listen()
+++ app.listen(8000, () => {
console.log('koa is running~')
})
listen(...args) {
debug('listen');
+++ const server = http.createServer(this.callback());
return server.listen(...args);
}
module.exports = class Application extends Emitter {
constructor(options) { // 省略 }
++ callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
+ const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
if (!this.ctxStorage) {
return this.handleRequest(ctx, fn);
}
return this.ctxStorage.run(ctx, async() => {
return await this.handleRequest(ctx, fn);
});
};
+ return handleRequest;
}
}
app.use()
1、注册中间件
+++ app.use((ctx, next) => {
console.log('匹配到中间件1')
// 5、返回数据
ctx.body = '中间件1'
})
module.exports = class Application extends Emitter {
constructor(options) { // 省略 }
+ use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
++ this.middleware.push(fn);
return this;
}
}
2、请求的处理过程
listen(...args) {
debug('listen');
+++ const server = http.createServer(this.callback());
return server.listen(...args);
}
module.exports = class Application extends Emitter {
constructor(options) { // 省略 }
+ callback() {
+ const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
+ const ctx = this.createContext(req, res);
if (!this.ctxStorage) {
+++ return this.handleRequest(ctx, fn);
}
return this.ctxStorage.run(ctx, async() => {
return await this.handleRequest(ctx, fn);
});
};
+ return handleRequest;
}
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
+++ return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
+ return dispatch(0)
// dispatch本质上就是next()
+ function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
++ return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
API
Application
- app.listen(port, callback):``,启动一个 HTTP 服务器并监听指定端口
- app.use():``,用来注册中间件的方法
- app.emit():``,触发事件并传递参数给监听器回调函数。
- app.on():``,注册一个事件监听器,当事件被触发时执行回调函数。
Context
属性:
- Node 原生对象
- ctx.req:原生 Node.js 的 request 对象。
- ctx.res:原生 Node.js 的 response 对象。
- Koa 封装对象
- ctx.request:Koa 封装后的请求对象,包含了请求头、请求体等信息。
- ctx.response:Koa 封装后的响应对象,包含了响应头、响应体等信息。
- 请求
- ctx.method:HTTP 请求的方法,如 GET、POST 等。
- ctx.url:请求的 URL 地址,不包含域名部分。
- ctx.path:请求的路径,不包含查询参数部分。
- 获取请求参数
- ctx.query:请求的 query 参数,以对象形式返回。
- ctx.params:请求的 params 参数
- ctx.request.body:请求的 post 参数,包括 json, x-www-form-urlencoded, form-data
- 响应
- ctx.body:响应的主体内容。
- ctx.status:响应的状态码,默认为 404。
- ctx.type:设置响应的
Content-Type
头部字段 - ctx.app:获取当前应用程序的实例
- ctx.cookies:Cookies 对象,用于读取和设置 Cookies。
Request
- ctx.request.body:``,请求的 post 参数,包括 json, x-www-form-urlencoded, form-data
Response
- ctx.response.body:``,等价于 ctx.body
中间件
@koa/router
koa-bodyparser
初始化:
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
- bodyParser():挂载 bodyParser
multer
安装: pnpm add @koa/multer
请求格式: multipart/form-data
应用: 文件上传
初始化:
const multer = require('@koa/multer')
- multer(options?):``,处理文件上传。它基于 busboy 构建,可帮助你方便地处理通过表单上传的文件
- options?:``,
- dest?:``,指定上传文件的保存路径
- storage?:``,详细指定上传文件的保存路径和文件名
- fileFilter?:``,用于过滤上传文件的回调函数
- options?:``,
- multer.diskStorage({destination, filename}):
,用于配置磁盘存储引擎的函数,可以作为
storage
选项的值 - multer.memoryStorage():``,用于配置内存存储引擎的函数
- upload.single(fieldname):
,用于处理单个文件上传的中间件。可以通过
req.file
访问上传的文件- fieldname:``,上传文件的字段名,必须与 HTML 表单中 input 标签的 name 属性相同
- upload.array(fieldname, maxCount?):
,用于处理多文件上传。可以通过
req.files
访问上传的文件- fieldname:``,上传文件的字段名,必须与 HTML 表单中 input 标签的 name 属性相同
- maxCount?:``,允许上传的最大文件数量,默认值为 Infinity
- 注意: 所有上传文件的字段名都必须一样
- upload.fields():
,用于处理文件上传的中间件函数。可以通过
req.files
访问上传的文件- fields:
[{name: fieldname, maxCount?},...]
,用于定义文件上传的配置 - 注意: 所有上传文件的字段名可以单独设置
- fields:
- upload.any():
,解析通过
form-data
格式请求的数据。可以通过req.body
获取解析后的数据- 注意: 不推荐使用
form-data
格式请求参数。通常使用form-data
格式上传文件
- 注意: 不推荐使用
@koa/multer
对 multer 的包装
koa-static
初始化:
const serve = require('koa-static')
app.use(serve('./xxx')) // xxx为静态服务器的根目录
- serve(root):``,返回一个静态服务器的中间件
- root:``,静态服务器的根目录